Часто при работе с пользовательскими данными приходится сталкиваться с тем, что данные предоставляются для работы без описания. При этом не известно, что содержательно представляют собой те или иные признаки, а известны лишь их типы: числовые, категориальные, номинальные переменные. Такие ситуации - не редкость при работе с «чувствительными» данными, например, в сфере банковской аналитики, HR-аналитики, сфере телекоммуникаций, страхования, здравоохранения, недвижимости или ритейла. Тем не менее, с такими данным нужно уметь работать, и нужно уметь проводить на них классические этапы анализа, в частности описательный анализ данных и визуализацию. Именно этим мы займемся на первой неделе.
В этом задании мы потренируемся делать описательный анализ и визуализацию так называемых «закрытых» данных - данных, которые предоставляются для анализа и построения моделей без описания. Выборка, с которой мы будем работать прилагается.
1 соответствует классу отток, -1 - классу не отток) – orange_small_churn_labels.trainorange_small_churn_data.trainСкачайте эти файлы на странице задания и переходите к выполнению задачи! Результатом выполнения задания является jupyter notebook. Перед отправкой результата убедитесь, что вы выполнили все пункты из задания и это можно понять из вашего notebook'а. Проверьте, что код написан понятно и аккуратно - это поможет вашим сокурсником лучше понять, что вы сделали, и оценить вашу работу.
1. Загрузите данные orange_small_churn_data.train и orange_small_churn_labels.train
import warnings
warnings.filterwarnings('ignore')
import seaborn as sns
from matplotlib import pyplot as plt
sns.set_style("darkgrid")
import pandas as pd
import numpy as np
from scipy import stats
import itertools
import random
features = pd.read_csv('orange_small_churn_data.train')
labels = pd.read_csv('orange_small_churn_labels.train',header= None,names=['target'])
data = pd.concat([features,labels],axis=1)
data.head()
2. Рассчитайте доли классов отток и не отток.
print(f"Количество объектов: {data.shape[0]}")
print(f'Доля класса "не отток": {(data.target==-1).mean()}')
print(f'Доля класса "отток": {(data.target==1).mean()}')
Посмотрим на долю отсутствующих значений. Для этого построим вспомогательный датасет, а затем heat map, где желтым отмечены пропущенные значения.
data_to_heat = data.isnull()
data_to_heat.head()
%%time
with plt.xkcd():
plt.figure(figsize=(20,14))
colors = ['#000099', '#ffff00']
sns.heatmap(data_to_heat,cmap = sns.color_palette(colors));
Видно, что пропущенных значений намного больше, чем заполненных ячеек.
3. Рассчитайте корреляции переменных с целевой функцией и проанализируйте полученные данные.
Обратите внимание на то, что целевая функция предсталвляет собой бинарную переменную. Теоретически, это не помешает рассчиать, например, корреляцию Пирсона между непрерывной переменной и целевой функцией, онднако анализиоовать полученные результаты будет сложно, корреляция Пирсона просто не рассчитана на такое применение. Кто уже забыл, как действовать в такой ситуации - вспоминить можно вот здесь: https://www.coursera.org/teach/stats-for-data-analysis/content/edit/lecture/prX3S/video-subtitles
data.dtypes.value_counts()
data_float = data.select_dtypes(include = ['float64']).copy()
data_float.head()
Разделим признаки по типам. Посмотрим на типы данных, рассмотрим тип float. Будем относить признаки с количеством уникальных значений меньше 20 к категориальным, остальные - к числовым. При этом совсем неинформативные признаки убираем из рассмотрения.
cols_float_cat = []
cols_float_num = []
for col in data_float.columns:
len_unique = np.unique(data[col].dropna()).shape[0]
if len_unique < 20:
print(col)
print(len_unique)
print('--------')
if len_unique > 1:
cols_float_cat.append(col)
else:
cols_float_num.append(col)
Рассмотрим признак с типом данных int.
data_int = data.select_dtypes(include=['int64']).copy()
data_int.head()
np.unique(data_int.Var73).shape[0]
Отнесем данный признак к числовым признакам.
Рассмотрим признаки с типом данных object.
data_not_num = data.select_dtypes(include=['object']).copy()
data_not_num.head()
Не будем рассмотривать неинформативные признаки.
cols_cat=[]
for col in data_not_num.columns.tolist():
len_unique = np.unique(data[col].dropna()).shape[0]
if len_unique > 1:
cols_cat.append(col)
else:
print(col)
print(len_unique)
print('-------')
data_cat=data[cols_cat+cols_float_cat]
data_cat.head()
data_num = data[cols_float_num+['Var73']]
data_num.head()
data_num_1 = data_num[data.target==1]
data_num_neg1 = data_num[data.target==-1]
col_num = data_num.columns.tolist()
col_num
diff_E=[]
for col in col_num[:-1]:
diff = abs(data_num_1[col].dropna().mean()-data_num_neg1[col].dropna().mean())/data_num[col].dropna().mean()
diff_E.append(diff)
diff_E_df = pd.DataFrame(np.array(diff_E).reshape(1,len(diff_E)),columns=col_num[:-1])
diff_E_df_sorted = diff_E_df.sort_values(by=0,axis=1,ascending=False)
diff_E_df_sorted
with plt.xkcd():
plt.figure(figsize=(16,10))
diff_E_df_sorted.iloc[0,:20].plot(kind='bar')
plt.title('Диаграмма взаимосвязи основных числовых признаков и отклика')
plt.ylabel('Normalized absolute value of differences between Ex for classes')
plt.grid()
plt.xlabel('features');
col_cat = data_cat.columns
col_cat
%%time
for col in col_cat:
print(f'{col}: number of not NAN values for class 1 :{data[data.target==1][col].dropna().shape[0]},number of not NAN values for class -1:{data[data.target==-1][col].dropna().shape[0]}')
print('-----------')
Видно, что данных по классам по каждому из признаков много, попробуем применить сам критерий.
%%time
phi_Cs = []
p_vs = []
proportion_of_cells_below5 = []
for col in col_cat:
ddf = pd.concat([data[col],data.target],axis=1).dropna()
cross = pd.crosstab(ddf[col], ddf.target)
chi2,p_v,_,expected_fr = stats.chi2_contingency(cross)
print(col)
below5=np.sum(expected_fr<5)/expected_fr.size
print(f'Доля ячеек в таблице частот, где значение меньше 5: {below5}')
print('--------')
phi_c = np.sqrt(chi2/(sum(sum(cross.values))*(np.min([cross.shape[0],cross.shape[1]])-1)))
phi_Cs.append(phi_c)
p_vs.append(p_v)
proportion_of_cells_below5.append(below5)
Видно, что есть много признаков, для которых в таблицах ожидаемых частот больше 20% ячеек со значениями меньше 5. Для таких признаков вышеуказанный критерий не применим. Ниже будут построены несколько диаграмм, на которых можно уведеть результаты применения критерия.
df_cat = pd.DataFrame([proportion_of_cells_below5,phi_Cs,p_vs,],columns=col_cat,index=['prop_below5','phi_c','p-value'])
df_cat
df_cat_sorted = df_cat.sort_values(by='phi_c',axis=1,ascending=False)
df_cat_sorted
with plt.xkcd():
plt.figure(figsize=(16,5))
df_cat_sorted.loc['prop_below5'].plot(kind='bar',color = 'red')
plt.title('Степень неприменимости критерия хи-квадрат')
plt.ylabel('доля ячеек таблицы, где частота меньше 5')
plt.grid()
plt.xlabel('features');
with plt.xkcd():
plt.figure(figsize=(16,5))
df_cat_sorted.loc['p-value'][df_cat_sorted.loc['prop_below5']<0.2].plot(kind='bar',color='grey')
plt.title('Достигаемые уровни значимости, когда применим критерий')
plt.ylabel('p-value')
plt.grid()
plt.xlabel('features');
with plt.xkcd():
plt.figure(figsize=(16,5))
df_cat_sorted.loc['phi_c'][df_cat_sorted.loc['prop_below5']<0.2][df_cat_sorted.loc['p-value']<0.05].plot(kind='bar',color = 'green')
plt.title('Категориальные признаки, для которых применим критерий и корреляция стат. значима')
plt.ylabel('V Крамера')
plt.grid()
plt.xlabel('features');
with plt.xkcd():
plt.figure(figsize=(16,5))
df_cat_sorted.loc['p-value'][df_cat_sorted.loc['prop_below5']<0.2][df_cat_sorted.loc['p-value']<0.05].plot(kind='bar',color='brown')
plt.title('Достигаемые уровни значимости, когда применим критерий и корреляция значима')
plt.ylabel('p-value')
plt.grid()
plt.xlabel('features');
В данной ситуации можно оценить сверху, как может повлиять поправка на множественную проверку гипотез. Воспользуемся методом Бонферони у домножим p-value на количество категориальных признаков (38) на максимальное значение p-value (4e-5). Получим величину намного меньше 0.05, то есть данная поправка ничего не меняет.
Можно заметить, что критерий хи-квадрат применим для небольшого количества признаков. Видно также, что даже даже статистически значимая по хи-квадрат корреляция, практически значима очень слабо.
Значимость признаков для классификации будет оцениваться далее визуально с помощью гистограмм условных распределений признаков по классам.
4. Визуализируйте данные: рассмотрите топ 20 числовых переменных, наиболее сильно коррелирующих с целевой функцией. Для этих переменных постройте:
Построим распределения признаков по классам.
color_dic={1:'red',-1:'blue'}
lbl_dic = {1:'отток',-1:'не отток'}
def two_hists(col,y,bins = None,nonzero=False,log=False,xlim = [0,0]):
fe_name = col.name
if xlim[1]==0 and xlim[0]==0:
left = col.min()
right = col.max()
else:
left = xlim[0]
right = xlim[1]
with plt.xkcd():
fig, axes = plt.subplots(nrows=1, ncols=2, figsize=(20, 5))
df = pd.DataFrame(np.vstack([col,y]).T,columns=[fe_name,'target'])
if nonzero:
df=df[df[fe_name]>0]
if log:
df[fe_name] = df[fe_name].apply(lambda x: np.log(x+1))
for idx, (cls, sub_df) in enumerate(df.groupby('target')):
ax = axes[idx]
if type(bins)==int:
ax.hist(sub_df[fe_name],color = color_dic[cls],bins=bins)
else:
ax.hist(sub_df[fe_name],color = color_dic[cls])
ax.set(xlabel=fe_name,ylabel='количество объектов')
ax.set_xlim(left, right)
ax.grid(True)
ax.legend([lbl_dic[cls]])
top_20_num_fe = diff_E_df_sorted.columns[:20].tolist()
top_20_num_fe
%%time
for col in top_20_num_fe:
two_hists(data[col],data['target'],nonzero=True)
Из гистограмм можно заметить, что распределения варьируются по классам для всех 20 признаков, при этом распределения некоторых признаков похожи между собой (Var 131 и Var 50). Из анализа корреляций можно будет понять взаимосвязь признаков, тогда некоторые можно исключить из рассмотрения как неинформативные. Также ясно, что нужно проверить на меньшем масштабе признаки 157,107,166,23,74,33, поскольку разность математических ожиданий для разных классов настроилась на выборосы.
two_hists(data[data.Var157<300].Var157,data[data.Var157<300].target,nonzero=True)
two_hists(data[data.Var107<50].Var107,data[data.Var107<50].target,nonzero=True)
two_hists(data[data.Var166<200].Var166,data[data.Var166<200].target,nonzero=True)
two_hists(data[data.Var23<100].Var23,data[data.Var23<100].target,nonzero=True)
two_hists(data[data.Var74<1000].Var74,data[data.Var74<1000].target,nonzero=True)
two_hists(data[data.Var33<1e6].Var33,data[data.Var33<1e6].target,nonzero=True)
Видно, что признак 74, вероятно, бесполезен.
Построим объекты в координатах пар признаков. Понятно, что если нет действительных пересечений, то построение невозможно.
def scatter_two_classes(fe1,fe2,y,log1=False,log2=False):
df = pd.concat([fe1,fe2,y],axis=1).dropna()
if log1:
df[df.columns.tolist()[0]]=df[df.columns.tolist()[0]].apply(lambda x: np.log(x+1))
if log2:
df[df.columns.tolist()[1]]=df[df.columns.tolist()[1]].apply(lambda x: np.log(x+1))
if df.shape[0]>0:
with plt.xkcd():
plt.figure(figsize=(10,7))
plt.grid()
plt.xlabel(df.columns.tolist()[0])
plt.ylabel(df.columns.tolist()[1])
for (cls, sub_df) in df.groupby('target'):
plt.scatter(sub_df.iloc[:,0],sub_df.iloc[:,1],c=color_dic[cls],label=lbl_dic[cls])
plt.legend()
else:
print(f'{df.columns.tolist()[:2]}: No real objects in these coords!')
%%time
for (col1,col2) in itertools.combinations(top_20_num_fe,2):
scatter_two_classes(fe1 = data[col1],fe2 = data[col2], y=data.target)
Можно заметить корреляцию, но мешают выбросы. Проверим корреляцию Пирсона.
plt.figure(figsize=(16,10))
with plt.xkcd():
sns.heatmap(data[top_20_num_fe].corr(method='pearson'));
Видно, что некоторые признаки скоррелированы. Попробуем перейти к логарифмированию, чтобы уменьшить влияние выбросов.
for (col1,col2) in itertools.combinations(top_20_num_fe,2):
scatter_two_classes(fe1 = data[col1],fe2 = data[col2], y=data.target,log1=True,log2=True)
Корреляция более ярко выражена. Построим таблицу корреляции Спирмана.
%%time
plt.figure(figsize=(16,10))
with plt.xkcd():
sns.heatmap(data[top_20_num_fe].corr(method='spearman'));
Видна сильная корреляция признаков (131 и 50), (177 и 114), (23,107,157 и 166), (77 и 12).
Проверим наличие признаков, для которых количество уникальных значение соответствует количеству ненулевых
ind =0
for col in col_num:
proportion = np.unique(data[col].dropna()).shape[0]/(data[col].dropna().shape[0])
if proportion == 1:
ind == 1
print(f'Признак, где количество уникальных значений равно количеству ненулевых: {col}')
if ind == 0:
print('Таких признаков нет')
ind =0
for col in col_cat:
proportion = np.unique(data[col].dropna()).shape[0]/(data[col].dropna().shape[0])
if proportion == 1:
ind == 1
print(f'Признак, где количество уникальных значений равно количеству ненулевых: {col}')
if ind == 0:
print('Таких признаков нет')
Из анализа построенных диаграмм можно прийти к выводу о предположительной наибольшей полезности признаков 131,77, 124,177,98 из рассмотренных. При этом наименьшую полезность, вероятно, имеют признаки 74,23,107,157.
5. Проделайте аналогичные шаги для случайно выбранных 10 числовых признаков.
Сгенерируем случайную подвыборку
random.seed(42)
smpl = random.sample(col_num,10)
smpl
Проверим на дублирование с уже рассмотренными признаками
ind=0
for fe in smpl:
if fe in top_20_num_fe:
ind=1
print(fe)
if ind==0:
print('Совпадений нет')
Проделаем аналогичные предыдущему пункту шаги
for col in smpl:
two_hists(data[col],data['target'],nonzero=True)
Поскольку выборка случайная, среди признаков попадаются полезные (к примеру, признак 36). Посмотрим в другом масштабе на признаки 129, 25, 60,24
two_hists(data[data.Var129<50].Var129,data[data.Var129<50].target,nonzero=True)
two_hists(data[data.Var25<1000].Var25,data[data.Var25<1000].target,nonzero=True)
two_hists(data[data.Var24<50].Var24,data[data.Var24<50].target,nonzero=True)
two_hists(data[data.Var60<100].Var60,data[data.Var60<100].target,nonzero=True)
Видно, что признаки 24,25,вероятно, бесполезны и, при этом, коррелируют.
for (col1,col2) in itertools.combinations(smpl,2):
scatter_two_classes(fe1 = data[col1],fe2 = data[col2], y=data.target)
plt.figure(figsize=(16,10))
with plt.xkcd():
sns.heatmap(data[smpl].corr(method='pearson'));
for (col1,col2) in itertools.combinations(smpl,2):
scatter_two_classes(fe1 = data[col1],fe2 = data[col2], y=data.target,log1=True,log2=True)
plt.figure(figsize=(16,10))
with plt.xkcd():
sns.heatmap(data[smpl].corr(method='spearman'));
Из анализа корреляций видны множественные корреляции, а также вероятные совпадения признаков (129, 66, 9). Отметим, что признаки (24,25) и (136,154) коррелируют, хотя и меньше.
Вероятно, наиболее полезными из рассмотренных признаков являются 154, 36, а бесполезными - 57,24,25.
6. Проделайте аналогичные шаги для 10 числовых признаков, наименее сильно коррелирующих с целевой переменной.
least_important_10_num_fe = diff_E_df_sorted.columns[-10:].tolist()
least_important_10_num_fe
for col in least_important_10_num_fe:
two_hists(data[col],data['target'],nonzero=True)
Анализ гистограмм показывает, что, в основном, распределения указанных признаков по классам почти идентичны, поэтому они и наименее значимы. Исследуем поподробнее признаки 123 и 113.
two_hists(data[data.Var123<1000].Var123,data[data.Var123<1000].target,nonzero=True)
two_hists(data[data.Var113<3e6][data.Var113>0].Var113,data[data.Var113<3e6][data.Var113>0].target,nonzero=True)
Данные признаки практически бесполезны.
for (col1,col2) in itertools.combinations(least_important_10_num_fe,2):
scatter_two_classes(fe1 = data[col1],fe2 = data[col2], y=data.target)
plt.figure(figsize=(16,10))
with plt.xkcd():
sns.heatmap(data[least_important_10_num_fe].corr(method='pearson'));
for (col1,col2) in itertools.combinations(least_important_10_num_fe,2):
scatter_two_classes(fe1 = data[col1],fe2 = data[col2], y=data.target,log1=True,log2=True)
plt.figure(figsize=(16,10))
with plt.xkcd():
sns.heatmap(data[least_important_10_num_fe].corr(method='spearman'));
Анализ диаграмм показывает наличие множественных корреляций, при этом признаки 104, 105 практически совпадают. При этом видно, почему признаки менее всего коррелируют с целевой переменной, в основном, их распределения не сильно зависят от класса.
Из данных признаков полезным, вероятно, является только 16, бесполезны признаки 163, 57,133,126,123,113.
7. Посмотрите на категориальные переменные: постройте и сравните гистограммы значений категориальных переменных для разных классов.
def two_hists_cat(col,y,max_bars = 30):
fe_name = col.name
num_unique =np.unique(np.array(col.dropna(),dtype=str)).shape[0]
df = pd.DataFrame(np.vstack([col,y]).T,columns=[fe_name,'target']).dropna()
df[fe_name] = df[fe_name].astype('str')
values = df[fe_name].value_counts().index[:max_bars]
with plt.xkcd():
fig, axes = plt.subplots(nrows=1, ncols=2, figsize=(20, 5))
for idx, (cls, sub_df) in enumerate(df.groupby('target')):
ax = axes[idx]
x = sub_df[fe_name].value_counts()
heights = [x[val] if val in x else 0 for val in values]
ax.bar(values, heights,color=color_dic[cls])
ax.set_xticklabels(labels = values, rotation = 90)
ax.set(xlabel=fe_name,ylabel='количество объектов')
ax.grid(True)
ax.legend([lbl_dic[cls]]);
%%time
for col in col_cat:
two_hists_cat(data[col],data['target'],40)
Из анализа гистограмм, можно сказать, что, вероятно, полезны признаки 192, 199, 200, 202, 214, 218, 225, 7, 30, 82, 87, 152, 172, бесполезными, предположительно, являются признаки 193, 194, 195, 196, 197, 201, 203, 206, 207, 210, 211, 219, 221, 223, 228, 229, 2, 4, 11, 26, 27, 29, 35, 44, 49, 65, 67, 90, 93, 100, 116, 122, 132, 138, 143, 155, 159, 161, 173, 181.
При этом можно заметить, что то признаков по критерию хи-квадрат практически не вошли в число потенциально полезных признаков. Причина здесь в том, что V Крамера для таких признаков не превосходил 0.1 (что означает очень слабая взаимосвязь), при этом критерий не был применим для признаков с большим количеством значений, который, кстати, вошли в перечень полезных.
Выводы по результатам предварительного анализа данных: